package refactoringtofuturetask.refactorings;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.Assignment;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.ChildListPropertyDescriptor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.Initializer;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.ParenthesizedExpression;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.ReturnStatement;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.ThisExpression;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclarationStatement;
import org.eclipse.jdt.core.dom.TypeParameter;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite.ImportRewriteContext;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.core.refactoring.CompilationUnitChange;
import org.eclipse.jdt.internal.corext.codemanipulation.ContextSensitiveImportRewriteContext;
import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility;
import org.eclipse.jdt.internal.corext.dom.ASTNodeFactory;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.Bindings;
import org.eclipse.jdt.internal.corext.dom.BodyDeclarationRewrite;
import org.eclipse.jdt.internal.corext.dom.LinkedNodeFinder;
import org.eclipse.jdt.internal.corext.dom.Selection;
import org.eclipse.jdt.internal.corext.refactoring.Checks;
import org.eclipse.jdt.internal.corext.refactoring.ParameterInfo;
import org.eclipse.jdt.internal.corext.refactoring.util.RefactoringASTParser;
import org.eclipse.jdt.internal.corext.refactoring.util.ResourceUtil;
import org.eclipse.jdt.internal.corext.refactoring.util.SelectionAwareSourceRangeComputer;
import org.eclipse.jdt.internal.corext.util.Messages;
import org.eclipse.jdt.internal.ui.text.correction.ASTResolving;
import org.eclipse.jdt.internal.ui.viewsupport.BasicElementLabels;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.ltk.core.refactoring.participants.ResourceChangeChecker;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.TextEditGroup;

import refactoringtofuturetask.util.FTRefactoringMessages;

@SuppressWarnings("restriction")
public class ConvertToFutureTaskRefactoring extends Refactoring {

	private ICompilationUnit fCUnit;
	private CompilationUnit fRoot;
	private int fSelectionStart;
	private int fSelectionLength;
	private List<ParameterInfo> fParameterInfos;
	private ConvertToFutureTaskAnalyzer fAnalyzer;
	private ImportRewrite fImportRewriter;
	private Set<String> fUsedNames;
	private int fDestinationIndex = 0;
	// either of type TypeDeclaration or AnonymousClassDeclaration
	private ASTNode fDestination;
	// either of type TypeDeclaration or AnonymousClassDeclaration
	private ASTNode[] fDestinations;
	private boolean fThrowRuntimeExceptions;
	private String fClassName;
	private ASTRewrite fRewriter;
	private AST fAST;

	private static class UsedNamesCollector extends ASTVisitor {
		private Set<String> result = new HashSet<String>();
		private Set<SimpleName> fIgnore = new HashSet<SimpleName>();

		public static Set<String> perform(ASTNode[] nodes) {
			UsedNamesCollector collector = new UsedNamesCollector();
			for (int i = 0; i < nodes.length; i++) {
				nodes[i].accept(collector);
			}
			return collector.result;
		}

		@Override
		public boolean visit(FieldAccess node) {
			Expression exp = node.getExpression();
			if (exp != null)
				fIgnore.add(node.getName());
			return true;
		}

		@Override
		public void endVisit(FieldAccess node) {
			fIgnore.remove(node.getName());
		}

		@Override
		public boolean visit(MethodInvocation node) {
			Expression exp = node.getExpression();
			if (exp != null)
				fIgnore.add(node.getName());
			return true;
		}

		@Override
		public void endVisit(MethodInvocation node) {
			fIgnore.remove(node.getName());
		}

		@Override
		public boolean visit(QualifiedName node) {
			fIgnore.add(node.getName());
			return true;
		}

		@Override
		public void endVisit(QualifiedName node) {
			fIgnore.remove(node.getName());
		}

		@Override
		public boolean visit(SimpleName node) {
			if (!fIgnore.contains(node))
				result.add(node.getIdentifier());
			return true;
		}

		@Override
		public boolean visit(TypeDeclaration node) {
			return visitType(node);
		}

		@Override
		public boolean visit(AnnotationTypeDeclaration node) {
			return visitType(node);
		}

		@Override
		public boolean visit(EnumDeclaration node) {
			return visitType(node);
		}

		private boolean visitType(AbstractTypeDeclaration node) {
			result.add(node.getName().getIdentifier());
			// don't dive into type declaration since they open a new
			// context.
			return false;
		}
	}

	/**
	 * Creates a new convert to FutureTask refactoring
	 * 
	 * @param unit
	 *            the compilation unit
	 * @param selectionStart
	 *            selection start
	 * @param selectionLength
	 *            selection end
	 */
	public ConvertToFutureTaskRefactoring(ICompilationUnit unit,
			int selectionStart, int selectionLength) {
		fCUnit = unit;
		fRoot = null;
		fSelectionStart = selectionStart;
		fSelectionLength = selectionLength;
		fClassName = "ExtractedFutureTask";
	}

	@Override
	public String getName() {
		return "Convert to FutureTask";
	}

	@Override
	public RefactoringStatus checkInitialConditions(IProgressMonitor pm)
			throws CoreException, OperationCanceledException {
		RefactoringStatus result = new RefactoringStatus();

		pm.beginTask("", 100); //$NON-NLS-1$

		if (fSelectionStart < 0 || fSelectionLength == 0)
			return mergeTextSelectionStatus(result);

		IFile[] changedFiles = ResourceUtil
				.getFiles(new ICompilationUnit[] { fCUnit });
		result.merge(Checks.validateModifiesFiles(changedFiles,
				getValidationContext()));
		if (result.hasFatalError())
			return result;
		result.merge(ResourceChangeChecker.checkFilesToBeChanged(changedFiles,
				new SubProgressMonitor(pm, 1)));

		if (fRoot == null) {
			fRoot = RefactoringASTParser.parseWithASTProvider(fCUnit, true,
					new SubProgressMonitor(pm, 99));
		}

		fImportRewriter = StubUtility.createImportRewrite(fRoot, true);
		fAST = fRoot.getAST();

		fAnalyzer = new ConvertToFutureTaskAnalyzer(fCUnit,
				Selection.createFromStartLength(fSelectionStart,
						fSelectionLength));
		fRoot.accept(fAnalyzer);

		fSelectionStart = fAnalyzer.getSelection().getOffset();
		fSelectionLength = fAnalyzer.getSelection().getLength();

		result.merge(fAnalyzer.checkInitialConditions(fImportRewriter));

		if (result.hasFatalError())
			return result;

		initializeParameterInfos();
		initializeUsedNames();
		initializeDestinations();

		return result;
	}

	@Override
	public RefactoringStatus checkFinalConditions(IProgressMonitor pm)
			throws CoreException, OperationCanceledException {
		pm.beginTask(
				FTRefactoringMessages.ConvertToFutureTask_checking_new_name, 2);
		pm.subTask("");

		RefactoringStatus result = checkClassName();
		result.merge(checkParameterNames());
		result.merge(checkVarargOrder());
		pm.worked(1);
		if (pm.isCanceled())
			throw new OperationCanceledException();

		BodyDeclaration node = fAnalyzer.getEnclosingBodyDeclaration();
		if (!(node instanceof MethodDeclaration || node instanceof Initializer))
			result.addWarning(FTRefactoringMessages.ConvertToFutureTask_selected_code);
		if (node != null) {
			fAnalyzer.checkInput(result, fClassName, fDestination);
			pm.worked(1);
		}
		pm.done();
		return result;
	}

	@Override
	public Change createChange(IProgressMonitor pm) throws CoreException {
		if (fClassName == null)
			return null;
		pm.beginTask("", 2); //$NON-NLS-1$
		try {
			fAnalyzer.aboutToCreateChange();
			BodyDeclaration declaration = fAnalyzer
					.getEnclosingBodyDeclaration();
			fRewriter = ASTRewrite.create(declaration.getAST());

			final CompilationUnitChange result = new CompilationUnitChange(
					FTRefactoringMessages.ConvertToFutureTask_change_name,
					fCUnit);
			result.setSaveMode(TextFileChange.KEEP_SAVE_STATE);

			MultiTextEdit root = new MultiTextEdit();
			result.setEdit(root);

			ASTNode[] selectedNodes = fAnalyzer.getSelectedNodes();
			fRewriter
					.setTargetSourceRangeComputer(new SelectionAwareSourceRangeComputer(
							selectedNodes, fCUnit.getBuffer(), fSelectionStart,
							fSelectionLength));

			TextEditGroup substituteDesc = new TextEditGroup(
					Messages.format(
							FTRefactoringMessages.ConvertToFutureTask_substitute_with_callable,
							BasicElementLabels.getJavaElementName(fClassName)));
			result.addTextEditGroup(substituteDesc);

			// MethodDeclaration mm = createNewMethod(selectedNodes,
			// fCUnit.findRecommendedLineSeparator(), substituteDesc);
			TypeDeclarationStatement tds = createFutureTaskClass(selectedNodes,
					fCUnit.findRecommendedLineSeparator(), substituteDesc);
			TextEditGroup insertDesc = new TextEditGroup(Messages.format(
					FTRefactoringMessages.ConvertToFutureTask_add_callable,
					BasicElementLabels.getJavaElementName(fClassName)));
			result.addTextEditGroup(insertDesc);
			if (fDestination == fDestinations[0]) {
				List stmts = ((Block) fDestination).statements();
				if (!stmts.isEmpty()) {
					Statement stmt = (Statement) stmts.get(0);
					ChildListPropertyDescriptor desc = (ChildListPropertyDescriptor) stmt
							.getLocationInParent();
					ListRewrite container = fRewriter.getListRewrite(
							fDestination, desc);
					container.insertBefore(tds, stmt, insertDesc);
				}
			} else if (fDestination == fDestinations[1]) {
				ChildListPropertyDescriptor desc = (ChildListPropertyDescriptor) declaration
						.getLocationInParent();
				ListRewrite container = fRewriter.getListRewrite(
						declaration.getParent(), desc);
				container.insertAfter(tds, declaration, insertDesc);
			} else {
				BodyDeclarationRewrite container = BodyDeclarationRewrite
						.create(fRewriter, fDestination);
				container.insert(tds.getDeclaration(), insertDesc);
			}

			if (fImportRewriter.hasRecordedChanges()) {
				TextEdit edit = fImportRewriter.rewriteImports(null);
				root.addChild(edit);
				result.addTextEditGroup(new TextEditGroup(
						FTRefactoringMessages.ConvertToFutureTask_organize_imports,
						new TextEdit[] { edit }));
			}
			root.addChild(fRewriter.rewriteAST());
			return result;
		} finally {
			pm.done();
		}
	}

	/**
	 * Checks if the new class name is a valid class name.
	 * 
	 * @return validation status
	 */
	public RefactoringStatus checkClassName() {
		return Checks.checkTypeName(fClassName, fCUnit);
	}

	/**
	 * Checks if the parameter names are valid.
	 * 
	 * @return validation status
	 */
	public RefactoringStatus checkParameterNames() {
		RefactoringStatus result = new RefactoringStatus();
		for (Iterator<ParameterInfo> iter = fParameterInfos.iterator(); iter
				.hasNext();) {
			ParameterInfo parameter = iter.next();
			result.merge(Checks.checkIdentifier(parameter.getNewName(), fCUnit));
			for (Iterator<ParameterInfo> others = fParameterInfos.iterator(); others
					.hasNext();) {
				ParameterInfo other = others.next();
				if (parameter != other
						&& other.getNewName().equals(parameter.getNewName())) {
					result.addError(Messages
							.format(FTRefactoringMessages.ConvertToFutureTask_error_sameParameter,
									BasicElementLabels.getJavaElementName(other
											.getNewName())));
					return result;
				}
			}
			if (parameter.isRenamed()
					&& fUsedNames.contains(parameter.getNewName())) {
				result.addError(Messages
						.format(FTRefactoringMessages.ConvertToFutureTask_error_nameInUse,
								BasicElementLabels.getJavaElementName(parameter
										.getNewName())));
				return result;
			}
		}
		return result;
	}

	/**
	 * Checks if varargs are ordered correctly.
	 * 
	 * @return validation status
	 */
	public RefactoringStatus checkVarargOrder() {
		for (Iterator<ParameterInfo> iter = fParameterInfos.iterator(); iter
				.hasNext();) {
			ParameterInfo info = iter.next();
			if (info.isOldVarargs() && iter.hasNext()) {
				return RefactoringStatus
						.createFatalErrorStatus(Messages
								.format(FTRefactoringMessages.ConvertToFutureTask_error_vararg_ordering,
										BasicElementLabels
												.getJavaElementName(info
														.getOldName())));
			}
		}
		return new RefactoringStatus();
	}

	public ICompilationUnit getCompilationUnit() {
		return fCUnit;
	}

	/**
	 * Returns the parameter infos.
	 * 
	 * @return a list of parameter infos.
	 */
	public List<ParameterInfo> getParameterInfos() {
		return fParameterInfos;
	}

	/**
	 * Sets the class name to be used for the extracted task.
	 * 
	 * @param name
	 *            the new class name.
	 */
	public void setMethodName(String name) {
		fClassName = name;
	}

	/**
	 * Sets whether the new method signature throws runtime exceptions.
	 * 
	 * @param throwRuntimeExceptions
	 *            flag indicating if the new method throws runtime exceptions
	 */
	public void setThrowRuntimeExceptions(boolean throwRuntimeExceptions) {
		fThrowRuntimeExceptions = throwRuntimeExceptions;
	}

	// ---- Helper methods
	// -----------------------------------------------------------------------

	private void initializeParameterInfos() {
		IVariableBinding[] arguments = fAnalyzer.getArguments();
		fParameterInfos = new ArrayList<ParameterInfo>(arguments.length);
		ASTNode root = fAnalyzer.getEnclosingBodyDeclaration();
		ParameterInfo vararg = null;
		for (int i = 0; i < arguments.length; i++) {
			IVariableBinding argument = arguments[i];
			if (argument == null)
				continue;
			VariableDeclaration declaration = ASTNodes.findVariableDeclaration(
					argument, root);
			boolean isVarargs = declaration instanceof SingleVariableDeclaration ? ((SingleVariableDeclaration) declaration)
					.isVarargs() : false;
			ParameterInfo info = new ParameterInfo(argument, getType(
					declaration, isVarargs), argument.getName(), i);
			if (isVarargs) {
				vararg = info;
			} else {
				fParameterInfos.add(info);
			}
		}
		if (vararg != null) {
			fParameterInfos.add(vararg);
		}
	}

	private void initializeUsedNames() {
		fUsedNames = UsedNamesCollector.perform(fAnalyzer.getSelectedNodes());
		for (Iterator<ParameterInfo> iter = fParameterInfos.iterator(); iter
				.hasNext();) {
			ParameterInfo parameter = iter.next();
			fUsedNames.remove(parameter.getOldName());
		}
	}

	private void initializeDestinations() {
		List<ASTNode> result = new ArrayList<ASTNode>();
		BodyDeclaration decl = fAnalyzer.getEnclosingBodyDeclaration();
		if (decl instanceof MethodDeclaration) {
			result.add(((MethodDeclaration) decl).getBody());
		} else if (decl instanceof Initializer) {
			result.add(((Initializer) decl).getBody());
		}
		ASTNode current = getNextParent(decl);
		result.add(current);
		if (decl instanceof MethodDeclaration || decl instanceof Initializer) {
			ITypeBinding binding = ASTNodes.getEnclosingType(current);
			ASTNode next = getNextParent(current);
			while (next != null && binding != null && binding.isNested()) {
				result.add(next);
				current = next;
				binding = ASTNodes.getEnclosingType(current);
				next = getNextParent(next);
			}
		}
		fDestinations = result.toArray(new ASTNode[result.size()]);
		fDestination = fDestinations[fDestinationIndex];
	}

	private ASTNode getNextParent(ASTNode node) {
		do {
			node = node.getParent();
		} while (node != null
				&& !(node instanceof AbstractTypeDeclaration || node instanceof AnonymousClassDeclaration));
		return node;
	}

	private String getType(VariableDeclaration declaration, boolean isVarargs) {
		String type = ASTNodes.asString(ASTNodeFactory.newType(declaration
				.getAST(), declaration, fImportRewriter,
				new ContextSensitiveImportRewriteContext(declaration,
						fImportRewriter)));
		if (isVarargs)
			return type + ParameterInfo.ELLIPSIS;
		else
			return type;
	}

	private RefactoringStatus mergeTextSelectionStatus(RefactoringStatus status) {
		status.addFatalError(FTRefactoringMessages.ConvertToFutureTask_no_set_of_statements);
		return status;
	}

	// ---- Code generation
	// -----------------------------------------------------------------------

	private TypeDeclarationStatement createFutureTaskClass(
			ASTNode[] selectedNodes, String lineDelimiter,
			TextEditGroup substitute) {
		TypeDeclaration decl = fAST.newTypeDeclaration();
		decl.setName(fAST.newSimpleName(fClassName));
		Type newTypeRS = fAST.newSimpleType(ASTNodeFactory.newName(fAST,
				"Callable"));
		decl.superInterfaceTypes().add(newTypeRS);
		decl.bodyDeclarations().addAll(createNewFieldDeclarations());
		decl.bodyDeclarations().add(createNewConstructorDeclaration());
		decl.bodyDeclarations().add(
				createCallMethodDeclaration(selectedNodes, lineDelimiter,
						substitute));

		fImportRewriter.addImport("java.util.concurrent.FutureTask");
		fImportRewriter.addImport("java.util.concurrent.Callable");

		TypeDeclarationStatement tDeclStmt = fAST
				.newTypeDeclarationStatement(decl);
		return tDeclStmt;
	}

	private List<FieldDeclaration> createNewFieldDeclarations() {
		ImportRewriteContext context = getImportRewrtieContext();

		List<FieldDeclaration> fields = new ArrayList<FieldDeclaration>();
		for (int i = 0; i < fParameterInfos.size(); i++) {
			ParameterInfo info = fParameterInfos.get(i);
			VariableDeclaration infoDecl = getVariableDeclaration(info);
			VariableDeclarationFragment fragment = fAST
					.newVariableDeclarationFragment();
			fragment.setName(fAST.newSimpleName(info.getNewName()));
			FieldDeclaration field = fAST.newFieldDeclaration(fragment);

			if (info.isNewVarargs()) {
				field.setType(fAST.newArrayType(ASTNodeFactory.newType(fAST,
						infoDecl, fImportRewriter, context)));
			} else {
				field.setType(ASTNodeFactory.newType(fAST, infoDecl,
						fImportRewriter, context));
			}
			fields.add(field);
		}
		return fields;
	}

	private MethodDeclaration createNewConstructorDeclaration() {
		MethodDeclaration result = fAST.newMethodDeclaration();
		result.setConstructor(true);

		ITypeBinding[] typeVariables = computeLocalTypeVariables();
		List<TypeParameter> typeParameters = result.typeParameters();
		for (int i = 0; i < typeVariables.length; i++) {
			TypeParameter parameter = fAST.newTypeParameter();
			parameter.setName(fAST.newSimpleName(typeVariables[i].getName()));
			ITypeBinding[] bounds = typeVariables[i].getTypeBounds();
			for (int j = 0; j < bounds.length; j++)
				if (!"java.lang.Object".equals(bounds[j].getQualifiedName())) //$NON-NLS-1$
					parameter.typeBounds().add(
							fImportRewriter.addImport(bounds[j], fAST));
			typeParameters.add(parameter);
		}

		result.setName(fAST.newSimpleName(fClassName));

		ImportRewriteContext context = getImportRewrtieContext();

		List<SingleVariableDeclaration> parameters = result.parameters();
		for (int i = 0; i < fParameterInfos.size(); i++) {
			ParameterInfo info = fParameterInfos.get(i);
			VariableDeclaration infoDecl = getVariableDeclaration(info);
			SingleVariableDeclaration parameter = fAST
					.newSingleVariableDeclaration();
			parameter.modifiers().addAll(
					ASTNodeFactory.newModifiers(fAST,
							ASTNodes.getModifiers(infoDecl)));
			parameter.setType(ASTNodeFactory.newType(fAST, infoDecl,
					fImportRewriter, context));
			parameter.setName(fAST.newSimpleName(info.getNewName()));
			parameter.setVarargs(info.isNewVarargs());
			parameters.add(parameter);
		}

		result.setBody(createConstructorBody(parameters));

		return result;
	}

	private Block createConstructorBody(List<SingleVariableDeclaration> fields) {
		Block b = fAST.newBlock();

		for (SingleVariableDeclaration decl : fields) {
			Assignment assign = fAST.newAssignment();
			FieldAccess fieldA = fAST.newFieldAccess();
			ThisExpression thisE = fAST.newThisExpression();
			fieldA.setExpression(thisE);
			fieldA.setName(fAST.newSimpleName(decl.getName().getIdentifier()));
			assign.setLeftHandSide(fieldA);
			assign.setRightHandSide(fAST.newSimpleName(decl.getName()
					.getIdentifier()));
			b.statements().add(fAST.newExpressionStatement(assign));
		}

		return b;
	}

	private MethodDeclaration createCallMethodDeclaration(
			ASTNode[] selectedNodes, String lineDelimiter,
			TextEditGroup substitute) {
		MethodDeclaration result = fAST.newMethodDeclaration();
		result.modifiers().addAll(
				ASTNodeFactory.newModifiers(fAST, Modifier.PUBLIC));
		result.setReturnType2((Type) ASTNode.copySubtree(fAST,
				fAnalyzer.getReturnType()));
		result.setName(fAST.newSimpleName("call"));

		ImportRewriteContext context = getImportRewrtieContext();

		List<Name> exceptions = result.thrownExceptions();
		ITypeBinding[] exceptionTypes = fAnalyzer
				.getExceptions(fThrowRuntimeExceptions);
		for (int i = 0; i < exceptionTypes.length; i++) {
			ITypeBinding exceptionType = exceptionTypes[i];
			exceptions.add(ASTNodeFactory.newName(fAST,
					fImportRewriter.addImport(exceptionType, context)));
		}

		result.setBody(createCallMethodBody(selectedNodes, substitute,
				result.getModifiers()));

		return result;
	}

	private Block createCallMethodBody(ASTNode[] selectedNodes,
			TextEditGroup substitute, int modifiers) {
		Block result = fAST.newBlock();
		ListRewrite statements = fRewriter.getListRewrite(result,
				Block.STATEMENTS_PROPERTY);

		// Locals that are not passed as an arguments since the extracted method
		// only
		// writes to them
		IVariableBinding[] methodLocals = fAnalyzer.getMethodLocals();
		for (int i = 0; i < methodLocals.length; i++) {
			if (methodLocals[i] != null) {
				result.statements().add(
						createDeclaration(methodLocals[i], null));
			}
		}

		for (Iterator<ParameterInfo> iter = fParameterInfos.iterator(); iter
				.hasNext();) {
			ParameterInfo parameter = iter.next();
			if (parameter.isRenamed()) {
				for (int n = 0; n < selectedNodes.length; n++) {
					SimpleName[] oldNames = LinkedNodeFinder.findByBinding(
							selectedNodes[n], parameter.getOldBinding());
					for (int i = 0; i < oldNames.length; i++) {
						fRewriter.replace(oldNames[i],
								fAST.newSimpleName(parameter.getNewName()),
								null);
					}
				}
			}
		}

		boolean extractsExpression = fAnalyzer.isExpressionSelected();
		ASTNode[] callNodes = createCallNodes(modifiers);
		ASTNode replacementNode;
		if (callNodes.length == 1) {
			replacementNode = callNodes[0];
		} else {
			replacementNode = fRewriter.createGroupNode(callNodes);
		}
		if (extractsExpression) {
			// if we have an expression then only one node is selected.
			ITypeBinding binding = fAnalyzer.getExpressionBinding();
			if (binding != null
					&& (!binding.isPrimitive() || !"void".equals(binding.getName()))) { //$NON-NLS-1$
				ReturnStatement rs = fAST.newReturnStatement();
				rs.setExpression((Expression) fRewriter
						.createMoveTarget(selectedNodes[0] instanceof ParenthesizedExpression ? ((ParenthesizedExpression) selectedNodes[0])
								.getExpression() : selectedNodes[0]));
				statements.insertLast(rs, null);
			} else {
				ExpressionStatement st = fAST
						.newExpressionStatement((Expression) fRewriter
								.createMoveTarget(selectedNodes[0]));
				statements.insertLast(st, null);
			}
			fRewriter
					.replace(
							selectedNodes[0].getParent() instanceof ParenthesizedExpression ? selectedNodes[0]
									.getParent() : selectedNodes[0], replacementNode, substitute);
		} else {
			if (selectedNodes.length == 1) {
				statements.insertLast(
						fRewriter.createMoveTarget(selectedNodes[0]),
						substitute);
				fRewriter
						.replace(selectedNodes[0], replacementNode, substitute);
			} else {
				ListRewrite source = fRewriter.getListRewrite(selectedNodes[0]
						.getParent(),
						(ChildListPropertyDescriptor) selectedNodes[0]
								.getLocationInParent());
				ASTNode toMove = source.createMoveTarget(selectedNodes[0],
						selectedNodes[selectedNodes.length - 1],
						replacementNode, substitute);
				statements.insertLast(toMove, substitute);
			}
			IVariableBinding returnValue = fAnalyzer.getReturnValue();
			if (returnValue != null) {
				ReturnStatement rs = fAST.newReturnStatement();
				rs.setExpression(fAST.newSimpleName(getName(returnValue)));
				statements.insertLast(rs, null);
			}
		}
		return result;
	}

	private ImportRewriteContext getImportRewrtieContext() {
		ASTNode enclosingBodyDeclaration = fAnalyzer
				.getEnclosingBodyDeclaration();
		ASTNode dest = fDestination;
		if (fDestination == fDestinations[0]) {
			dest = fDestinations[1];
		}
		while (enclosingBodyDeclaration != null
				&& enclosingBodyDeclaration.getParent() != dest) {
			enclosingBodyDeclaration = enclosingBodyDeclaration.getParent();
		}
		ImportRewriteContext context = new ContextSensitiveImportRewriteContext(
				enclosingBodyDeclaration, fImportRewriter);
		return context;
	}

	private ITypeBinding[] computeLocalTypeVariables() {
		List<ITypeBinding> result = new ArrayList<ITypeBinding>(
				Arrays.asList(fAnalyzer.getTypeVariables()));
		for (int i = 0; i < fParameterInfos.size(); i++) {
			ParameterInfo info = fParameterInfos.get(i);
			processVariable(result, info.getOldBinding());
		}
		IVariableBinding[] methodLocals = fAnalyzer.getMethodLocals();
		for (int i = 0; i < methodLocals.length; i++) {
			processVariable(result, methodLocals[i]);
		}
		return result.toArray(new ITypeBinding[result.size()]);
	}

	private void processVariable(List<ITypeBinding> result,
			IVariableBinding variable) {
		if (variable == null)
			return;
		ITypeBinding binding = variable.getType();
		if (binding != null && binding.isParameterizedType()) {
			ITypeBinding[] typeArgs = binding.getTypeArguments();
			for (int args = 0; args < typeArgs.length; args++) {
				ITypeBinding arg = typeArgs[args];
				if (arg.isTypeVariable() && !result.contains(arg)) {
					ASTNode decl = fRoot.findDeclaringNode(arg);
					if (decl != null
							&& decl.getParent() instanceof MethodDeclaration) {
						result.add(arg);
					}
				}
			}
		}
	}

	private VariableDeclaration getVariableDeclaration(ParameterInfo parameter) {
		return ASTNodes.findVariableDeclaration(parameter.getOldBinding(),
				fAnalyzer.getEnclosingBodyDeclaration());
	}

	private VariableDeclarationStatement createDeclaration(
			IVariableBinding binding, Expression intilizer) {
		VariableDeclaration original = ASTNodes.findVariableDeclaration(
				binding, fAnalyzer.getEnclosingBodyDeclaration());
		VariableDeclarationFragment fragment = fAST
				.newVariableDeclarationFragment();
		fragment.setName((SimpleName) ASTNode.copySubtree(fAST,
				original.getName()));
		fragment.setInitializer(intilizer);
		VariableDeclarationStatement result = fAST
				.newVariableDeclarationStatement(fragment);
		result.modifiers().addAll(
				ASTNode.copySubtrees(fAST, ASTNodes.getModifiers(original)));
		result.setType(ASTNodeFactory.newType(fAST, original, fImportRewriter,
				new ContextSensitiveImportRewriteContext(original,
						fImportRewriter)));
		return result;
	}

	private String getName(IVariableBinding binding) {
		for (Iterator<ParameterInfo> iter = fParameterInfos.iterator(); iter
				.hasNext();) {
			ParameterInfo info = iter.next();
			if (Bindings.equals(binding, info.getOldBinding())) {
				return info.getNewName();
			}
		}
		return binding.getName();
	}
	
	private ASTNode[] createCallNodes(int modifiers) {
		List<ASTNode> result= new ArrayList<ASTNode>(2);

		IVariableBinding[] locals= fAnalyzer.getCallerLocals();
		for (int i= 0; i < locals.length; i++) {
			result.add(createDeclaration(locals[i], null));
		}

		MethodInvocation invocation= fAST.newMethodInvocation();
		invocation.setName(fAST.newSimpleName("call"));
		ASTNode typeNode= ASTResolving.findParentType(fAnalyzer.getEnclosingBodyDeclaration());
		RefactoringStatus status= new RefactoringStatus();
		while (fDestination != fDestinations[0] && fDestination != typeNode) {
			fAnalyzer.checkInput(status, "call", typeNode);
			if (!status.isOK()) {
				SimpleName destinationTypeName= fAST.newSimpleName(ASTNodes.getEnclosingType(fDestination).getName());
				if ((modifiers & Modifier.STATIC) == 0) {
					ThisExpression thisExpression= fAST.newThisExpression();
					thisExpression.setQualifier(destinationTypeName);
					invocation.setExpression(thisExpression);
				} else {
					invocation.setExpression(destinationTypeName);
				}
				break;
			}
			typeNode= typeNode.getParent();
		}

		List<Expression> arguments= invocation.arguments();
		for (int i= 0; i < fParameterInfos.size(); i++) {
			ParameterInfo parameter= fParameterInfos.get(i);
			arguments.add(ASTNodeFactory.newName(fAST, parameter.getOldName()));
		}

		ASTNode call;
		int returnKind= fAnalyzer.getReturnKind();
		switch (returnKind) {
			case ConvertToFutureTaskAnalyzer.ACCESS_TO_LOCAL:
				IVariableBinding binding= fAnalyzer.getReturnLocal();
				if (binding != null) {
					VariableDeclarationStatement decl= createDeclaration(binding, invocation);
					call= decl;
				} else {
					Assignment assignment= fAST.newAssignment();
					assignment.setLeftHandSide(ASTNodeFactory.newName(fAST,
							fAnalyzer.getReturnValue().getName()));
					assignment.setRightHandSide(invocation);
					call= assignment;
				}
				break;
			case ConvertToFutureTaskAnalyzer.RETURN_STATEMENT_VALUE:
				ReturnStatement rs= fAST.newReturnStatement();
				rs.setExpression(invocation);
				call= rs;
				break;
			default:
				call= invocation;
		}

		if (call instanceof Expression && !fAnalyzer.isExpressionSelected()) {
			call= fAST.newExpressionStatement((Expression)call);
		}
		result.add(call);

		// We have a void return statement. The code looks like
		// extracted();
		// return;
		if (returnKind == ConvertToFutureTaskAnalyzer.RETURN_STATEMENT_VOID && !fAnalyzer.isLastStatementSelected()) {
			result.add(fAST.newReturnStatement());
		}
		return result.toArray(new ASTNode[result.size()]);
	}
}
